/**
 * Development repository: https://github.com/kaisalmen/WWOBJLoader
 */

import {
  FileLoader,
  Object3D,
  Loader
} from "../../build/three.module.js";

import {OBJLoader2Parser} from "./obj2/OBJLoader2Parser.js";
import {MeshReceiver} from "./obj2/shared/MeshReceiver.js";
import {MaterialHandler} from "./obj2/shared/MaterialHandler.js";

/**
 * Creates a new OBJLoader2. Use it to load OBJ data from files or to parse OBJ data from arraybuffer or text.
 *
 * @param {LoadingManager} [manager] The loadingManager for the loader to use. Default is {@link LoadingManager}
 * @constructor
 */
const OBJLoader2 = function (manager) {

  Loader.call(this, manager);

  this.parser = new OBJLoader2Parser();

  this.modelName = '';
  this.instanceNo = 0;
  this.baseObject3d = new Object3D();

  this.materialHandler = new MaterialHandler();
  this.meshReceiver = new MeshReceiver(this.materialHandler);

  // as OBJLoader2 is no longer derived from OBJLoader2Parser, we need to override the default onAssetAvailable callback
  const scope = this;
  const defaultOnAssetAvailable = function (payload) {

    scope._onAssetAvailable(payload);

  };

  this.parser.setCallbackOnAssetAvailable(defaultOnAssetAvailable);

};

OBJLoader2.OBJLOADER2_VERSION = '3.2.0';
console.info('Using OBJLoader2 version: ' + OBJLoader2.OBJLOADER2_VERSION);


OBJLoader2.prototype = Object.assign(Object.create(Loader.prototype), {

  constructor: OBJLoader2,

  /**
   * See {@link OBJLoader2Parser.setLogging}
   * @return {OBJLoader2}
   */
  setLogging: function (enabled, debug) {

    this.parser.setLogging(enabled, debug);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setMaterialPerSmoothingGroup}
   * @return {OBJLoader2}
   */
  setMaterialPerSmoothingGroup: function (materialPerSmoothingGroup) {

    this.parser.setMaterialPerSmoothingGroup(materialPerSmoothingGroup);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setUseOAsMesh}
   * @return {OBJLoader2}
   */
  setUseOAsMesh: function (useOAsMesh) {

    this.parser.setUseOAsMesh(useOAsMesh);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setUseIndices}
   * @return {OBJLoader2}
   */
  setUseIndices: function (useIndices) {

    this.parser.setUseIndices(useIndices);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setDisregardNormals}
   * @return {OBJLoader2}
   */
  setDisregardNormals: function (disregardNormals) {

    this.parser.setDisregardNormals(disregardNormals);
    return this;

  },

  /**
   * Set the name of the model.
   *
   * @param {string} modelName
   * @return {OBJLoader2}
   */
  setModelName: function (modelName) {

    this.modelName = modelName ? modelName : this.modelName;
    return this;

  },

  /**
   * Set the node where the loaded objects will be attached directly.
   *
   * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to
   * @return {OBJLoader2}
   */
  setBaseObject3d: function (baseObject3d) {

    this.baseObject3d = (baseObject3d === undefined || baseObject3d === null) ? this.baseObject3d : baseObject3d;
    return this;

  },

  /**
   * Add materials as associated array.
   *
   * @param {Object} materials Object with named {@link Material}
   * @param overrideExisting boolean Override existing material
   * @return {OBJLoader2}
   */
  addMaterials: function (materials, overrideExisting) {

    this.materialHandler.addMaterials(materials, overrideExisting);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setCallbackOnAssetAvailable}
   * @return {OBJLoader2}
   */
  setCallbackOnAssetAvailable: function (onAssetAvailable) {

    this.parser.setCallbackOnAssetAvailable(onAssetAvailable);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setCallbackOnProgress}
   * @return {OBJLoader2}
   */
  setCallbackOnProgress: function (onProgress) {

    this.parser.setCallbackOnProgress(onProgress);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setCallbackOnError}
   * @return {OBJLoader2}
   */
  setCallbackOnError: function (onError) {

    this.parser.setCallbackOnError(onError);
    return this;

  },

  /**
   * See {@link OBJLoader2Parser.setCallbackOnLoad}
   * @return {OBJLoader2}
   */
  setCallbackOnLoad: function (onLoad) {

    this.parser.setCallbackOnLoad(onLoad);
    return this;

  },

  /**
   * Register a function that is called once a single mesh is available and it could be altered by the supplied function.
   *
   * @param {Function} [onMeshAlter]
   * @return {OBJLoader2}
   */
  setCallbackOnMeshAlter: function (onMeshAlter) {

    this.meshReceiver._setCallbacks(this.parser.callbacks.onProgress, onMeshAlter);
    return this;

  },

  /**
   * Register a function that is called once all materials have been loaded and they could be altered by the supplied function.
   *
   * @param {Function} [onLoadMaterials]
   * @return {OBJLoader2}
   */
  setCallbackOnLoadMaterials: function (onLoadMaterials) {

    this.materialHandler._setCallbacks(onLoadMaterials);
    return this;

  },

  /**
   * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer.
   *
   * @param {string}  url A string containing the path/URL of the file to be loaded.
   * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument.
   * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes.
   * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument.
   * @param {function} [onMeshAlter] Called after every single mesh is made available by the parser
   */
  load: function (url, onLoad, onFileLoadProgress, onError, onMeshAlter) {

    const scope = this;
    if (onLoad === null || onLoad === undefined || !(onLoad instanceof Function)) {

      const errorMessage = 'onLoad is not a function! Aborting...';
      scope.parser.callbacks.onError(errorMessage);
      throw errorMessage;

    } else {

      this.parser.setCallbackOnLoad(onLoad);

    }

    if (onError === null || onError === undefined || !(onError instanceof Function)) {

      onError = function (event) {

        let errorMessage = event;

        if (event.currentTarget && event.currentTarget.statusText !== null) {

          errorMessage = 'Error occurred while downloading!\nurl: ' + event.currentTarget.responseURL + '\nstatus: ' + event.currentTarget.statusText;

        }

        scope.parser.callbacks.onError(errorMessage);

      };

    }

    if (!url) {

      onError('An invalid url was provided. Unable to continue!');

    }

    const urlFull = new URL(url, window.location.href).href;
    let filename = urlFull;
    const urlParts = urlFull.split('/');
    if (urlParts.length > 2) {

      filename = urlParts[urlParts.length - 1];
      this.path = urlParts.slice(0, urlParts.length - 1).join('/') + '/';

    }

    if (onFileLoadProgress === null || onFileLoadProgress === undefined || !(onFileLoadProgress instanceof Function)) {

      let numericalValueRef = 0;
      let numericalValue = 0;
      onFileLoadProgress = function (event) {

        if (!event.lengthComputable) return;

        numericalValue = event.loaded / event.total;

        if (numericalValue > numericalValueRef) {

          numericalValueRef = numericalValue;
          const output = 'Download of "' + url + '": ' + (numericalValue * 100).toFixed(2) + '%';
          scope.parser.callbacks.onProgress('progressLoad', output, numericalValue);

        }

      };

    }

    this.setCallbackOnMeshAlter(onMeshAlter);
    const fileLoaderOnLoad = function (content) {

      scope.parser.callbacks.onLoad(scope.parse(content), "OBJLoader2#load: Parsing completed");

    };

    const fileLoader = new FileLoader(this.manager);
    fileLoader.setPath(this.path || this.resourcePath);
    fileLoader.setResponseType('arraybuffer');
    fileLoader.load(filename, fileLoaderOnLoad, onFileLoadProgress, onError);

  },

  /**
   * Parses OBJ data synchronously from arraybuffer or string and returns the {@link Object3D}.
   *
   * @param {arraybuffer|string} content OBJ data as Uint8Array or String
   * @return {Object3D}
   */
  parse: function (content) {

    // fast-fail in case of illegal data
    if (content === null || content === undefined) {

      throw 'Provided content is not a valid ArrayBuffer or String. Unable to continue parsing';

    }

    if (this.parser.logging.enabled) {

      console.time('OBJLoader parse: ' + this.modelName);

    }

    // Create default materials beforehand, but do not override previously set materials (e.g. during init)
    this.materialHandler.createDefaultMaterials(false);

    // code works directly on the material references, parser clear its materials before updating
    this.parser.setMaterials(this.materialHandler.getMaterials());

    if (content instanceof ArrayBuffer || content instanceof Uint8Array) {

      if (this.parser.logging.enabled) console.info('Parsing arrayBuffer...');
      this.parser.execute(content);

    } else if (typeof (content) === 'string' || content instanceof String) {

      if (this.parser.logging.enabled) console.info('Parsing text...');
      this.parser.executeLegacy(content);

    } else {

      this.parser.callbacks.onError('Provided content was neither of type String nor Uint8Array! Aborting...');

    }

    if (this.parser.logging.enabled) {

      console.timeEnd('OBJLoader parse: ' + this.modelName);

    }

    return this.baseObject3d;

  },

  _onAssetAvailable: function (payload) {

    if (payload.cmd !== 'assetAvailable') return;

    if (payload.type === 'mesh') {

      const meshes = this.meshReceiver.buildMeshes(payload);
      for (const mesh of meshes) {

        this.baseObject3d.add(mesh);

      }

    } else if (payload.type === 'material') {

      this.materialHandler.addPayloadMaterials(payload);

    }

  }

});

export {OBJLoader2};
